package core.comunication;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;

import org.apache.log4j.Logger;

import core.enums.Errors;
import core.util.COREServiceRegistry;
import core.util.SockUtil;

/**
 * Questa classe si occupa del trasferimento file lato client. Il Client si occupa di inviare il file mentre il server riceve ( PUSH )
 * Il procedimento del trasferimento file è un processo che passa prima ad una fase di negoziazione  al fine di stabilire la porta di invo/ricezione file.
 * Dato che il trasferimento è basato su UDP, il file viene diviso in chunk.
 * @author Vincenzo Merlo, Scuderi Giovanni Luca, Paolo Giuffrida
 *
 * 
 */
public class FileClient {

	private final Logger logger = COREServiceRegistry.getLogger(FileClient.class.toString()); // istanza logger log4j
	private InetAddress ip = null; // indirizzo del server con cui comunicare
	private File file = null; // file di riferimento da comunicare
	private int port = -1; // porta su quale avverrà il trasferimento
	private long residuo; // varaibile usata per calcolare il riempimento del file. Succede che fissata una dimensione del pacchetto, l'ultimo pacchetto da trasmettere sia di dimensione inferiore al pacchetto contenitore, quindi lo si deve riempire.
	private int index; // indice del chunk corrente.

	/**
	 * Costruttore di default che inizializza ip server, porta e file da trasferire
	 * @param ip indirizzo del server
	 * @param port porta di trasferimento
	 * @param file file da traferire
	 */
	public FileClient(InetAddress ip, int port, File file) {
		this.ip = ip;
		this.file = file;
		this.port = port;
		logger.debug("Istanzio FileClient");
	}

	/**
	 * Metodo che inizializza il socket di comunicazione e messaggistica.
	 * Inizio comunicazione, comunicando file da trasferire
	 * Creo il socket di ascolto e invio per il file
	 * @return 1 se il file è stato trasferito correttamente, altrimenti ritorna un error code @see Errors
	 */
	public int run() {
		

		boolean fault = false;
		int nRetrive = 0;
		try {

			//Fase di comunicazione : "handshake" sulla porta di comunicazione

			//inizializzazione degli stream per la serializzazione della comunicazione
			ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
			ObjectOutputStream objectStream = new ObjectOutputStream(byteOut);
			//inizializzo l'oggetto che voglio inviare
			MSGRequestUpload request = new MSGRequestUpload();
			request.setFileName(file.getName());
			request.setAddr(InetAddress.getLocalHost());
			request.setPort(port);
			
			objectStream.writeObject(request);

			//costruzione del pacchetto datagram da inviare
			DatagramPacket packet = new DatagramPacket(
					byteOut.toByteArray(), byteOut.toByteArray().length,
					ip,
					port);

			//Buffer per l'invio del nome del file int|filename.hash
			byte[] buffer = new byte[10000];

			//Costruzione datagram da inviare
			DatagramPacket packer = new DatagramPacket(
					buffer,
					10000);
			
			DatagramSocket socket = null;
			do {
				fault = false;
				try {
					//inizio comunicazione
					logger.debug("Server " + ip + ":" + port + " ti voglio inviare il file " + file.getName());
					socket = new DatagramSocket();
					socket.send(packet);
					socket.close();
					
					//Apro un socket sull porta port + 1 e mi metto in attesa della risposta
					socket = new DatagramSocket(port + 1);
					socket.setSoTimeout(SockUtil.timeout);
					socket.receive(packer);
				} catch (SocketTimeoutException e) {
					//se la comunicazione fallisce, provo rinvio e incremento il numero di retry effettuati
					logger.debug("Timeout invio file");
					nRetrive++;
					if (nRetrive < SockUtil.nRetrive) {
						fault = true;
					} else {
						return Errors.FileTransmissionFailure.code();
					}
				} finally {
					socket.close();
				}
			} while (fault);

			
			logger.debug(this.getClass().getName() + " ho un messaggio in ingresso");
			
			//inizializzo comunicazione per ricevere la porta su cui verrà trasferito il file
			ByteArrayInputStream input = new ByteArrayInputStream(
					packer.getData(), packer.getOffset(), packer.getLength());
			ObjectInputStream oIn = new ObjectInputStream(input);

			//mi setto la porta di comunicazione su cui trasferiro' il file
			Object ackFromServer = oIn.readObject();
			
			
			
			if ( !(ackFromServer instanceof MSGRequestUploadAck) ) 
				return Errors.AckObjectError.code();

			logger.debug("Port di ascolto " + ((MSGRequestUploadAck)ackFromServer).getPort());

			socket.close();
			//comincio il trasferimento file
			return sendFile(file, ((MSGRequestUploadAck)ackFromServer).getPort());

		} catch (SocketException e) {
			e.printStackTrace();
			logger.debug(e.getStackTrace());
			return Errors.SocketException.code();
		} catch (IOException e) {
			e.printStackTrace();
			logger.debug(e.getStackTrace());
			return Errors.IOException.code();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			logger.debug(e.getStackTrace());
			return Errors.ClassNotFoundException.code();
		}


	}

	/*
	 * Metodo che si occupa del trasferimento file vero e proprio. Il file viene spezzettato in chunks e inviato sulla porta che ha scelto il server 
	 * @return 1 se il trasferimento file è avvenuto con successo altrimenti ritorna un codice Errors @see Errors
	 */

	private int sendFile(File file, int port) {
		int c = 0;
		//prelevo la dimensione del chunk dal settaggio del file
		Long dim = new Long(SockUtil.chunkSize); //size of chunk
		index = 0;
		//inizio la serializzazione / divisione de file in chunk
		FileInputStream fileStream;
		try {
			fileStream = new FileInputStream(file);

			ByteArrayOutputStream byteOut = new ByteArrayOutputStream();

			long fileSize = file.length();
			residuo = fileSize;
			//calcolo il numero di chunk
			long quote = (fileSize / SockUtil.chunkSize);

			if (file.length() > SockUtil.chunkSize) {
				dim = new Long(SockUtil.chunkSize);
			} else {
				dim = fileSize;
			}

			logger.debug("quote: " + quote);
			//comincio la trasmissione dei chunk; leggo i byte, li metto in un chunk e li trasmetto
			while ((-1 != (c = fileStream.read()))) {
				byteOut.write(c);
				if ((byteOut.size() == dim)) {
					if ((dim = sendChunk(byteOut.toByteArray(), dim, port)) < 0) {
						return Errors.FileTransmissionFailure.code();
					} else {
						byteOut.reset();
					}
				}
			}
			//controllo per l'ultimo pacchetto e per il primo, dove si potrebbero avere problemi sulla dimensione del chunk
			// la variabile riempimento ci serve a far si che la trasmissione si concluda correttamente anche nei casi particolati
			if (quote == 0 && file.length() >= SockUtil.chunkSize) {  //ovvero se il file � multiplo esatto di  @see SockUtil.chunkSize
				if ((dim = sendChunk(new byte[0], dim, port)) < 0) {
					return Errors.FileTransmissionFailure.code();
				}
			}

		} catch (IOException e) {
			logger.debug(this.getClass().getCanonicalName() + " " + e.getClass().getCanonicalName()
					+ " " + e.getStackTrace());
			return Errors.IOException.code();
		}

		return 1;

	}

	/*
	 * Il metodo senchunk si occupa di incapsulare il pacchetto e inviarlo al server
	 * @param byteOut
	 * @param dim dimensione del frame reale da inviare che verrà riadattato all'interno
	 * @param port Porta di trasferimento
	 */
	private long sendChunk(byte[] byteOut, Long dim, int port) throws IOException {
		//System.out.println("residuo " + residuo);
		DatagramSocket socket;
		DatagramPacket sndPacket;
		DatagramPacket rcvPacket;
		ChunkSend chunk;
		int nRetrieve = 0;
		boolean fault;
		do {
			//preparazione della comunicazione
			fault = false;
			socket = new DatagramSocket();
			socket.setSoTimeout(SockUtil.timeout);
			chunk = new ChunkSend(file.getName(), index, byteOut); //i = number of packet
			ByteArrayOutputStream byteChuck = new ByteArrayOutputStream();
			ObjectOutputStream oStream = new ObjectOutputStream(byteChuck);
			oStream.writeObject(chunk);
			oStream.flush();
			sndPacket = new DatagramPacket(
					byteChuck.toByteArray(), byteChuck.toByteArray().length,
					ip,
					port);
			//invio pacchetto
			socket.send(sndPacket);
			logger.trace("Invio chunck " + index + " " + byteChuck.toByteArray().length);
			//inizio comunicazione di riscontro e verifica
			rcvPacket = new DatagramPacket(new byte[1000], 1000); // così il limite massimo del nome del file è 50 caratteri in quanto in java ogni carattere è 2 byte
			try {
				rcvPacket = new DatagramPacket(new byte[1000], 1000);
				socket.receive(rcvPacket);

				ByteArrayInputStream input = new ByteArrayInputStream(
						rcvPacket.getData(), rcvPacket.getOffset(), rcvPacket.getLength());

				ObjectInputStream oIn = new ObjectInputStream(input);

				logger.trace("ricevo l'ack del chunck - lo controllo");
				//controllo ack relativo all'index
				checkAck(oIn, index, file.getName());

				//sequenza di chiusura comunicazione
				residuo -= dim;
				dim = (residuo > SockUtil.chunkSize) ? SockUtil.chunkSize : residuo;
				index++;
				byteChuck.reset();
				oStream.reset();
				oStream.close();
				byteChuck.close();

			} catch (SocketTimeoutException e) {
				logger.debug("Timeout FileClient " + ip + "  chunk " + index + " - retrive: " + nRetrieve);
				fault = true;
				nRetrieve++;
				if (nRetrieve == SockUtil.nRetrive) {
					logger.debug("Numero di retrive superato - invio annullato");
					socket.close();
					return Errors.SocketTimeoutException.code();
				}
			} finally {
				socket.close();
			}

		} while (fault);

		return dim;
	}

	/*
	 * Metodo che si occupa della verifica dell'ack
	 * @param oIn stream di ricezione
	 * @param index indice del pacchetto da riscontrare
	 */
	private void checkAck(ObjectInputStream oIn, int index, String fileName) throws AckWrongException {
		try {
			//controllo se l'ack è relativo al pacchetto ricevuto
			ChunkSendAck ackCheck = new ChunkSendAck();
			ackCheck.setFileName(fileName);
			ackCheck.setPacketIndex(index);
			
			Object ack = oIn.readObject();
			if (!(ack instanceof ChunkSendAck && ackCheck.equals((ChunkSendAck)ack)))
				throw new AckWrongException();
			
		} catch (Exception e) {
			e.printStackTrace();
			throw new AckWrongException();
		}
		
		

	}
}


/*
 * Classe eccezione di ritorno in caso di Ack errato
 */
class AckWrongException extends SocketTimeoutException {

	/**
	 *
	 */
	private static final long serialVersionUID = -5428131077073707118L;

	AckWrongException(String s) {
		super(s);
	}

	AckWrongException() {
		super("Ricevuto un ack sbagliato");
	}
}



